/////////////////////////////////////////////////////////////////////////////
// Name:        tts_macos.cpp
// Purpose:     A wxTextToSpeech handler for the Mac OS X Speech Manager
// Author:      Julian Smart
// Modified by: 
// Created:     2009-02-10
// RCS-ID:      
// Copyright:   (c) Julian Smart
// Licence:     New BSD License
/////////////////////////////////////////////////////////////////////////////

#include "wx/wx.h"

#ifdef __WXMAC__

#include "wx/filename.h"
#include "wx/textfile.h"
#include "wx/datstrm.h"
#include "wx/wfstream.h"
#include "wx/dir.h"
#include "wx/log.h"
#include "wx/utils.h"
#include "wx/mac/private.h"

#include "tts/tts_macosx.h"

#include "SpeechSynthesis.h"

/*
 * wxTTSMacOSXData
 */

class wxTTSMacOSXData
{
public:
    wxTTSMacOSXData() { m_channel = NULL; m_buffer = NULL; }
    ~wxTTSMacOSXData() { ResetData(); }

    void ResetData();

    SpeechChannel   m_channel;
    void*           m_buffer;
};

void wxTTSMacOSXData::ResetData()
{
    if (m_channel)
    {
        SpeechStatusInfo status;
        
        if ((GetSpeechInfo( m_channel, soStatus, (void*) &status ) == noErr)
             && (status.outputBusy ))
        {
            StopSpeech(m_channel);
        }

        DisposeSpeechChannel(m_channel);
        m_channel = NULL;
    }
    if (m_buffer)
    {
        delete[] (char*) m_buffer;
        m_buffer = NULL;
    }
}

// Delete the channel after speech is done, otherwise we might not close the file
// if we're writing to a file.
static void wxTTSMacOSXSpeechDone(SpeechChannel channel, void *data)
{
    wxTTSMacOSXHandler *handler = reinterpret_cast<wxTTSMacOSXHandler *>(data);
    if (handler && handler->GetData())
    {
        wxASSERT(channel == handler->GetData()->m_channel);

        handler->GetData()->ResetData();
    }
}

/*
 * wxTTSMacOSXHandler implementation
 */

IMPLEMENT_DYNAMIC_CLASS( wxTTSMacOSXHandler, wxTTSHandler )

wxTTSMacOSXHandler::wxTTSMacOSXHandler(): wxTTSHandler(wxT("Apple Speech Manager"), _("Apple Speech Manager"))
{
    Init();
}

wxTTSMacOSXHandler::~wxTTSMacOSXHandler()
{
    if (m_data)
        delete m_data;
}

// Member initialisation
void wxTTSMacOSXHandler::Init()
{
    m_data = new wxTTSMacOSXData;

    // Initialize generic properties

    SetVoice(wxT("")); // ?? No concept of default voice since can install any voice
    SetSpeed(170);      // Speed in words per minute, 80-390
    SetVolume(50);
    SetPitch(50);       // 0-100
    SetVoiceVariant(wxT("None"));

    SetProperty(wxTTS_PROP_INPUT_FORMAT, wxTTS_PROP_INPUT_FORMAT_TEXT);
    SetProperty(wxTTS_PROP_NO_SUBSTITUTIONS, false);
    SetProperty(wxTTS_PROP_SIMPLIFY_PUNCTUATION, false);
    SetProperty(wxTTS_PROP_TEXT_ENCODING, wxString(wxT("utf-8")));
    SetProperty(wxTTS_PROP_ENGINE_WEBSITE, wxString(wxT("http://www.microsoft.com/speech")));
    SetProperty(wxTTS_PROP_PRIORITY, 0);

    // Initialize Mac OS X-specific properties, if any
}

// Speak the text
bool wxTTSMacOSXHandler::Speak(const wxString& text, int options)
{
    return DoSpeak(text, false, options);
}

// Speak the file
bool wxTTSMacOSXHandler::SpeakFile(const wxString& filename, int options, const wxString& waveFilename)
{
    return DoSpeak(filename, true, options, waveFilename);
}

// Speak the text or file
bool wxTTSMacOSXHandler::DoSpeak(const wxString& textOrFilename, bool isFilename, int options, const wxString& waveFilename)
{
    if (IsSpeaking())
    {
        Stop();
    }
    
    SetOptions(options);

    // Check we're not trying to speak sychronously - not supported by this handler.
    wxASSERT( (options & wxTTS_SPEAK_SYNC) == 0 );

    if (options & wxTTS_SPEAK_SYNC)
        return false;

    if (options & (wxTTS_SPEAK_ASYNC|wxTTS_SPEAK_SYNC_YIELD))
    {
        // TODO: set async flag for native speech synthesis
    }

    // Get the flags that we will pass to the transformer
    int transformationFlags = CreateTransformerOptions();

    wxString inputFormat = GetPropertyString(wxTTS_PROP_INPUT_FORMAT);
    if (inputFormat == wxTTS_PROP_INPUT_FORMAT_HTML || inputFormat == wxTTS_PROP_INPUT_FORMAT_SSML)
    {
        transformationFlags |= (wxTTS_TRANSFORM_HTML_TO_TEXT|wxTTS_TRANSFORM_SPEAK_ORDERED_LIST);
    }
    else if (inputFormat == wxTTS_PROP_INPUT_FORMAT_NATIVE)
    {
        transformationFlags |= wxTTS_TRANSFORM_PASS_RAW_TEXT;
    }
    else
    {
        transformationFlags |= wxTTS_TRANSFORM_PASS_RAW_TEXT;
    }

    m_data->ResetData();

    // Change to the current voice
    wxString currentVoice = GetVoice();

    // We find the voiceSpec by enumerating the voices and stopping at the correct one.
    VoiceSpec voiceSpec;
    int i;
    short theVoiceCount;
    
    bool foundVoice = false;
    if (!currentVoice.IsEmpty() && CountVoices( &theVoiceCount ) == noErr)
    {
        VoiceDescription description;
        
        for (i = 1; i <= theVoiceCount; ++i)
        {
            if ( (GetIndVoice( i, &voiceSpec ) == noErr)  &&
                 (GetVoiceDescription( &voiceSpec, &description, sizeof(description) ) == noErr ) )
            {
                wxString name = wxMacMakeStringFromPascal(description.name);
                if (name == currentVoice)
                {
                    foundVoice = true;
                    break;
                }

            }
        }
    }

    if (foundVoice)
    {
        if (NewSpeechChannel(& voiceSpec, & m_data->m_channel) != noErr)
            return false;
    }
    else
    {
        if (NewSpeechChannel(NULL, & m_data->m_channel) != noErr)
            return false;
    }

    if (!m_data->m_channel)
        return false;

    int rate = GetSpeed();
    int pitch = GetPitch();
    SetSpeechRate(m_data->m_channel, IntToFixed(rate));
    SetSpeechPitch(m_data->m_channel, IntToFixed(pitch));

    Fixed fVolume = FixRatio(GetVolume(), 100);
    SetSpeechInfo(m_data->m_channel, soVolume, &fVolume);

#if 0
    // This results in a crash in MT3BEngineTask::Done, at least on Mac OS 10.4.11, so disabling for now.
    SetSpeechInfo(m_data->m_channel, soRefCon, this);
    SetSpeechInfo(m_data->m_channel, soSpeechDoneCallBack, reinterpret_cast<void *>(wxTTSMacOSXSpeechDone));
#endif

    if (!waveFilename.IsEmpty())
    {
        CFURLRef url = CFURLCreateWithFileSystemPath(
            kCFAllocatorDefault,
            wxMacCFStringHolder(waveFilename, wxLocale::GetSystemEncoding()),
            kCFURLHFSPathStyle,
            false ); //false == not a directory
        

        OSErr err = SetSpeechInfo(m_data->m_channel, soOutputToFileWithCFURL, url);
        CFRelease(url);

        if (err != noErr)
            return false;
    }

    wxString speechText;
        
    if (!isFilename)
    {
        wxString text(textOrFilename);
        if (GetTransformer())
            GetTransformer()->TransformString(textOrFilename, text, transformationFlags);
        
        speechText = text;
    }
    else
    {
        // First do some substitutions, if we have a transformer.
        // The return value is the name of the file we're actually going to speak,
        // which may be a temporary file.
        m_tempFilename = wxEmptyString;
        wxString filename = DoTransformation(textOrFilename, transformationFlags);

        {
            wxTextFile textFile;
            if (textFile.Open(filename))
            {
                for (i = 0; i < (int) textFile.GetLineCount(); i++)
                {
                    speechText += textFile[i];
                    speechText += wxT("\n");
                }
            }
        }
        if (!m_tempFilename.IsEmpty())
        {
            wxRemoveFile(m_tempFilename);
            m_tempFilename = wxEmptyString;
        }
    }

    if (!speechText.IsEmpty())
    {
        wxCharBuffer buf = speechText.mb_str(wxConvLocal);
        if (buf)
        {
            char* b = new char[strlen(buf)+1];
            strcpy(b, buf);
            m_data->m_buffer = (void*) b;

            if (SpeakText(m_data->m_channel, m_data->m_buffer, strlen(buf)) != noErr)
                return false;
        }

        // We've initiated speaking, now see if we need to wait until
        // it's finished
        if (options & wxTTS_SPEAK_SYNC_YIELD)
        {
            // Psuedo-asyncronous - processes events while writing.
            do
            {
                wxYield();
                wxMilliSleep(5);
            }
            while (IsSpeaking());
        }
        
        return true;
    }

    return false;
}

// Is it speaking?
bool wxTTSMacOSXHandler::IsSpeaking() const
{
    SpeechStatusInfo status;
    
    if ((GetSpeechInfo(m_data->m_channel, soStatus, (void*) &status ) == noErr)
        && (status.outputBusy ))
    {
        return true;
    }
    else
        return false;
}

// Stop speaking
bool wxTTSMacOSXHandler::Stop()
{
    if (IsSpeaking() || IsPaused())
    {
        StopSpeech(m_data->m_channel);
    }

    return true;
}

// Is it paused?
bool wxTTSMacOSXHandler::IsPaused() const
{
    SpeechStatusInfo status;
    
    if ((GetSpeechInfo(m_data->m_channel, soStatus, (void*) &status ) == noErr)
        && (status.outputPaused ))
    {
        return true;
    }
    else
        return false;
}

// Pause
bool wxTTSMacOSXHandler::Pause()
{
    if (!IsPaused() && IsSpeaking())
    {
        PauseSpeechAt(m_data->m_channel, kImmediate);
        return true;
    }
    else
        return false;
}

// Resume
bool wxTTSMacOSXHandler::Resume()
{
    if (IsPaused())
    {
        ContinueSpeech(m_data->m_channel);
        return true;
    }
    else
        return false;
}

// Get a list of the available voices
wxArrayString wxTTSMacOSXHandler::GetAvailableVoices() const
{
    wxArrayString arr;

    VoiceSpec newSpec;
    int i;
    short theVoiceCount;
    
    if (CountVoices( &theVoiceCount ) == noErr)
    {
        VoiceDescription description;
        
        for (i = 1; i <= theVoiceCount; ++i)
        {
            if ( (GetIndVoice( i, &newSpec ) == noErr)  &&
                 (GetVoiceDescription( &newSpec, &description, sizeof(description) ) == noErr ) )
            {
                wxString descr = wxMacMakeStringFromPascal(description.name);
                arr.Add(descr);
            }
        }
    }

    return arr;
}

// Get a list of the available voice variants
wxArrayString wxTTSMacOSXHandler::GetAvailableVoiceVariants() const
{
    wxArrayString variants;
    return variants;
}

// Does this handler have the specified capability?
bool wxTTSMacOSXHandler::HasCapability(wxTTSCapability capability) const
{
    if (capability == wxTTS_CAP_PROGRAM_LOCATION)
        return false;
    if (capability == wxTTS_CAP_DATA_LOCATION)
        return false;
    if (capability == wxTTS_CAP_WRITE_WAVE_FILE)
        return true;
    if (capability == wxTTS_CAP_SPEAK_HTML) // by virtue of HTML to text conversion
        return true;
    if (capability == wxTTS_CAP_VOICE_VARIATIONS)
        return false;
    if (capability == wxTTS_CAP_VOICE)
        return true;
    if (capability == wxTTS_CAP_PITCH)
        return true;
    if (capability == wxTTS_CAP_SPEED)
        return true;
    if (capability == wxTTS_CAP_VOLUME)
        return true;
    if (capability == wxTTS_CAP_SSML)
        return false;

    return false;
}

// Get the default voice
wxString wxTTSMacOSXHandler::GetDefaultVoice() const
{
    wxArrayString arr = GetAvailableVoices();
    if (arr.GetCount() > 0)
        return arr[0];
    else
        return wxEmptyString;
}

// Get the default voice variant
wxString wxTTSMacOSXHandler::GetDefaultVoiceVariant() const
{
    return wxT("None");
}

// Initialize the engine. Can call multiple times.
bool wxTTSMacOSXHandler::Initialize()
{
    if (!GetInitialized())
    {
        SetInitialized(true);
    }
    return true;
}

// Clean up the engine. Can call multiple times.
bool wxTTSMacOSXHandler::Uninitialize()
{
    if (GetInitialized())
    {
        Stop();
    }

    return true;
}

#endif
    // __WXMAC__
